iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Software Development

Roblox Studio AI 協作開發大全系列 第 16

第 16 章:道具、能力與冷卻時間

  • 分享至 

  • xImage
  •  

第 15 章加入敵人後,玩家第一次需要「應對威脅」。如果玩家只能走路、跳躍和逃跑,敵人會讓關卡有壓力,但玩法選擇仍然很少。本章要加入第一個主動能力:Wind Dash

Wind Dash 是一個短暫加速能力。玩家可以用鍵盤、gamepad 或手機按鈕觸發它,在幾秒內提高移動速度,穿過敵人巡邏區或逃離追擊。這個能力看起來簡單,但它會帶出 Roblox 遊戲中非常常見的一組問題:輸入在哪裡處理?能力效果誰決定?cooldown 放 client 還是 server?手機玩家怎麼用?

本章的答案是:client 負責輸入與顯示,server 負責驗證與效果。

本章目標

完成本章後,你會得到:

  • 一個 WindCharm Tool。
  • 一個跨裝置 WindDash InputAction。
  • 鍵盤 Q、gamepad ButtonX、手機 HUD 按鈕都能觸發能力。
  • RequestAbility RemoteEvent 用來送出使用意圖。
  • AbilityService 在 server 驗證 Tool 與 cooldown。
  • 玩家使用能力後短時間提高移動速度。
  • cooldown 期間不能重複使用。
  • HUD 顯示能力狀態與冷卻回饋。
  • TweenService 用來做 cooldown 視覺動畫。

Creator Store 中取得模型或工具資產的按鈕。

圖 16-1 外部 Tool 資產插入專案後必須檢查階層、Script 與權限,再加入能力和冷卻時間邏輯。

Context Pack

本章使用下列 Roblox 官方文件作為 context:

本章採用這些文件中的幾個原則:

  • 跨裝置輸入應該優先使用 Input Action System,而不是只處理鍵盤。
  • Tool 可以代表玩家擁有的道具或能力。
  • UserInputService 是 client-side service,不應該拿來做 server 權威判斷。
  • debounce / cooldown 是避免能力重複觸發的必要設計。
  • TweenService 可以做 UI 動畫,但 cooldown 是否完成要由 server 判斷。

開場情境

現在島上有一隻 IslandSentinel_01。玩家如果只是走過去,可能會被追上並扣血。Guide 決定給玩家一個風之護符:

WindCharm
使用後 2.5 秒內提高移動速度。
每 6 秒只能使用一次。

這個能力有三個設計目的:

  • 讓玩家有主動選擇。
  • 讓敵人區域變成可規劃的挑戰。
  • 讓我們建立後面商店與升級系統會用到的能力架構。

本章不做火球,原因很實際。火球需要投射物、命中判定、敵人血量、特效同步與傷害驗證,會讓章節一次引入太多系統。先做速度強化,讀者能專注在輸入、cooldown 與 client/server 分工。

第一個 Prompt

請 Assistant 建立第一個能力系統 prototype。

【Ch16 主任務|建立 WindDash 能力】
我們要繼續開發 Roblox Studio 專案「AI Adventure Island」。

現有背景:
- 專案中已有 StarterGui.AdventureHUD。
- ReplicatedStorage.Remotes 已經存在。
- ServerScriptService 已經包含 server-side service。
- 第 15 章新增了一個名為 IslandSentinel_01 的敵人。
- 目前不要加入 DataStore、商店購買、投射物戰鬥、敵人血條或升級功能。

請建立第一個名為 WindDash 的主動能力:
1. 在 StarterPack 建立名為 WindCharm 的 Tool。
2. 將 WindCharm.RequiresHandle 設為 false。
3. 如果 ReplicatedStorage.Inputs.PlayContext 不存在,請建立它。
4. 在 PlayContext 底下建立名為 WindDash 的 InputAction。
5. WindDash 必須是 Bool action。
6. 加入以下輸入綁定:
   - 鍵盤:Q
   - Gamepad:ButtonX
   - 觸控:StarterGui.AdventureHUD 裡名為 DashButton 的 HUD 按鈕
7. 在 ReplicatedStorage.Remotes 建立名為 RequestAbility 的 RemoteEvent。
8. 在 ReplicatedStorage.Remotes 建立名為 AbilityUpdate 的 RemoteEvent。
9. 建立 ServerScriptService.AbilityService。
10. AbilityService 必須在 server 驗證:
   - ability 名稱是 WindDash
   - 玩家在 Backpack 或 Character 中持有 WindCharm
   - ability 不在 cooldown 中
   - 玩家角色包含 Humanoid
11. WindDash 通過驗證時:
   - 將 cooldown 設為 6 秒
   - 將 Humanoid.WalkSpeed 提高到 28,持續 2.5 秒
   - 如果角色仍然有效,再恢復先前的 WalkSpeed
   - 透過 AbilityUpdate:FireClient 通知 HUD 顯示 cooldown
12. 如果 WindDash 因 cooldown 被拒絕,請透過 AbilityUpdate:FireClient 傳送剩餘時間。
13. 建立或更新 AdventureHUD 的 LocalScript,讓它:
   - 在 InputAction.Pressed 時傳送 RequestAbility:FireServer("WindDash")
   - 在 WindCharm.Activated 時也傳送相同請求
   - 讓 DashButton 顯示 cooldown 狀態
   - 使用 TweenService 製作簡單的 cooldown bar 或遮罩動畫

重要限制:
- client 只能傳送操作意圖及更新 UI。
- WindDash 是否成功由 server 決定。
- 不要讓 client 直接設定 WalkSpeed。
- 不要把 UI tween 的動畫時間當成真正的 cooldown。
- 所有 ability 邏輯都集中在一個 server-side AbilityService。

這個 prompt 明確要求同一個能力有多種輸入來源,但只有一條 server 流程:

InputAction 或 Tool.Activated
-> RequestAbility
-> AbilityService
-> AbilityUpdate

不要讓 Q 鍵、手機按鈕和 Tool 各自呼叫不同程式。那樣現在看起來能跑,後面一加商店、升級或技能替換就會出現三套邏輯。

Assistant 可能產生的結果

Assistant 可能會建立這樣的結構:

StarterPack
└── WindCharm (Tool)

ReplicatedStorage
├── Inputs
│   └── PlayContext
│       └── WindDash (InputAction)
│           ├── KeyboardBinding
│           ├── GamepadBinding
│           └── TouchBinding
└── Remotes
    ├── RequestAbility (RemoteEvent)
    └── AbilityUpdate (RemoteEvent)

StarterGui
└── AdventureHUD
    ├── DashButton
    ├── DashCooldownBar
    └── AbilityHUDController (LocalScript)

ServerScriptService
└── AbilityService

AbilityService 的核心常數可以先集中放在上方:

local WIND_DASH_COOLDOWN = 6
local WIND_DASH_DURATION = 2.5
local WIND_DASH_WALK_SPEED = 28

server 端 cooldown 可以用 table 記錄:

local cooldownEndsByPlayer = {}

第一版 server-side 能力流程可能像這樣:

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local remotes = ReplicatedStorage:WaitForChild("Remotes")
local requestAbility = remotes:WaitForChild("RequestAbility")
local abilityUpdate = remotes:WaitForChild("AbilityUpdate")

local WIND_DASH_COOLDOWN = 6
local WIND_DASH_DURATION = 2.5
local WIND_DASH_WALK_SPEED = 28

local cooldownEndsByPlayer = {}

local function getCharacterHumanoid(player)
	local character = player.Character
	if not character then
		return nil
	end

	return character:FindFirstChildWhichIsA("Humanoid")
end

local function playerHasWindCharm(player)
	local backpack = player:FindFirstChild("Backpack")
	local character = player.Character

	if backpack and backpack:FindFirstChild("WindCharm") then
		return true
	end

	if character and character:FindFirstChild("WindCharm") then
		return true
	end

	return false
end

local function getRemainingCooldown(player)
	local cooldownEndsAt = cooldownEndsByPlayer[player]
	if not cooldownEndsAt then
		return 0
	end

	return math.max(0, cooldownEndsAt - os.clock())
end

local function startCooldown(player)
	cooldownEndsByPlayer[player] = os.clock() + WIND_DASH_COOLDOWN
end

local function useWindDash(player)
	if not playerHasWindCharm(player) then
		abilityUpdate:FireClient(player, "WindDashRejected", "WindCharmMissing", 0)
		return
	end

	local remainingCooldown = getRemainingCooldown(player)
	if remainingCooldown > 0 then
		abilityUpdate:FireClient(player, "WindDashCooldown", remainingCooldown)
		return
	end

	local humanoid = getCharacterHumanoid(player)
	if not humanoid or humanoid.Health <= 0 then
		abilityUpdate:FireClient(player, "WindDashRejected", "InvalidCharacter", 0)
		return
	end

	startCooldown(player)
	abilityUpdate:FireClient(player, "WindDashStarted", WIND_DASH_COOLDOWN)

	local originalWalkSpeed = humanoid.WalkSpeed
	humanoid.WalkSpeed = WIND_DASH_WALK_SPEED

	task.delay(WIND_DASH_DURATION, function()
		if humanoid.Parent and humanoid.Health > 0 then
			humanoid.WalkSpeed = originalWalkSpeed
		end
	end)
end

requestAbility.OnServerEvent:Connect(function(player, abilityName)
	if abilityName ~= "WindDash" then
		return
	end

	useWindDash(player)
end)

Players.PlayerRemoving:Connect(function(player)
	cooldownEndsByPlayer[player] = nil
end)

這段程式有幾個重要特徵:

  • client 傳的是能力名稱,不傳 WalkSpeed。
  • server 自己檢查 Tool 與 cooldown。
  • cooldown 用 os.clock() 判斷,不看 UI 動畫。
  • 玩家離開時清掉 table。

client 端只做輸入與 UI:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local Players = game:GetService("Players")

local player = Players.LocalPlayer
local remotes = ReplicatedStorage:WaitForChild("Remotes")
local requestAbility = remotes:WaitForChild("RequestAbility")
local abilityUpdate = remotes:WaitForChild("AbilityUpdate")

local inputs = ReplicatedStorage:WaitForChild("Inputs")
local playContext = inputs:WaitForChild("PlayContext")
local windDashAction = playContext:WaitForChild("WindDash")

local hud = script.Parent
local dashButton = hud:WaitForChild("DashButton")
local cooldownBar = hud:WaitForChild("DashCooldownBar")

local function requestWindDash()
	requestAbility:FireServer("WindDash")
end

windDashAction.Pressed:Connect(requestWindDash)
dashButton.Activated:Connect(requestWindDash)

local function connectTool(tool)
	if tool.Name ~= "WindCharm" then
		return
	end

	tool.Activated:Connect(requestWindDash)
end

local backpack = player:WaitForChild("Backpack")
for _, child in backpack:GetChildren() do
	connectTool(child)
end
backpack.ChildAdded:Connect(connectTool)

local function playCooldownTween(duration)
	cooldownBar.Size = UDim2.fromScale(1, 1)

	local tween = TweenService:Create(
		cooldownBar,
		TweenInfo.new(duration, Enum.EasingStyle.Linear),
		{ Size = UDim2.fromScale(0, 1) }
	)

	tween:Play()
end

abilityUpdate.OnClientEvent:Connect(function(eventName, value)
	if eventName == "WindDashStarted" then
		dashButton.Text = "Dash"
		playCooldownTween(value)
	elseif eventName == "WindDashCooldown" then
		dashButton.Text = string.format("%.1fs", value)
	elseif eventName == "WindDashRejected" then
		dashButton.Text = "No Dash"
	end
end)

這段 client code 不是完整 UI 實作,但它展示正確方向:同一個 requestWindDash() 可以被 InputAction、HUD button、Tool.Activated 共用。

元件拆解

Tool

Tool 是 Roblox 中可被 Humanoid 裝備的物件。玩家的 Tool 通常放在 Backpack;要讓玩家出生時就有 Tool,可以把它放在 StarterPack

本章的 WindCharm 不是武器,也不需要拿在手上,所以可以設定:

RequiresHandle = false
CanBeDropped = false

Tool 在本章有兩個作用:

  • 讓玩家看得到自己擁有能力。
  • 讓 server 可以檢查玩家是否真的有 WindCharm

不要只靠 HUD 按鈕代表玩家有能力。HUD 可以被 client 修改;Tool 是否存在則可以由 server 檢查。

InputAction

InputAction 代表「玩家想做什麼」,不是代表某一顆鍵。

ReplicatedStorage 的 InputContext 內建立名為 CharacterSprint 的 InputAction。

圖 16-2 InputAction 放在 InputContext 內,讓鍵盤、觸控與手把 binding 共用同一個遊戲動作,而不是各寫一套能力邏輯。

本章的 action 是:

WindDash

它可以被多種 input binding 觸發:

Q
ButtonX
DashButton

這樣做比 UserInputService.InputBegan 只檢查 Q 更好,因為本章一開始就支援手機與 gamepad。手機輸入不能等到最後才補,否則 UI、能力流程與測試方式都可能要重寫。

UserInputService

UserInputService 仍然很重要,但它是 client-side service。它適合:

  • 偵測玩家目前偏好的輸入類型。
  • 處理特殊鍵盤或滑鼠輸入。
  • 做 device-specific UI 調整。

本章不把 UserInputService 當成主要能力入口,是因為 Input Action System 更符合跨裝置能力設計。

RemoteEvent

RequestAbility 是 client 到 server 的使用意圖:

requestAbility:FireServer("WindDash")

這不是請 server 相信 client。server 收到後仍要檢查:

  • ability name 是否合法。
  • 玩家是否有 WindCharm
  • cooldown 是否結束。
  • character 與 Humanoid 是否有效。

AbilityUpdate 則是 server 到 client 的結果通知:

abilityUpdate:FireClient(player, "WindDashStarted", WIND_DASH_COOLDOWN)

這和前面任務章的 QuestUpdate 很像:server 決定結果,client 顯示結果。

Cooldown

cooldown 的權威資料放在 server:

cooldownEndsByPlayer[player] = os.clock() + WIND_DASH_COOLDOWN

不要把 cooldown 只寫在 HUD 裡。HUD 可以提醒玩家,但不能當作規則本身。

如果 cooldown 只在 client,惡意 client 可以跳過等待,連續要求加速。server 端 cooldown 是最基本的防線。

TweenService

TweenService 在本章只用來做視覺回饋,例如讓 cooldown bar 從滿格慢慢縮到 0。

TweenService:Create(cooldownBar, TweenInfo.new(duration), {
	Size = UDim2.fromScale(0, 1),
})

它不能用來判斷能力是否真的恢復。原因很簡單:UI 動畫可能被取消、重播、延遲或被玩家端修改,但 server time 才是規則。

Playtest

本章要測四個面向。

第一輪:鍵盤

  1. 按 Play。
  2. 確認 Backpack 或工具列有 WindCharm
  3. 按 Q。
  4. 玩家速度應短時間提高。
  5. 立刻再按 Q。
  6. HUD 應顯示 cooldown,server 不應再次加速。
  7. 等 6 秒後再按 Q,能力應再次成功。

第二輪:Tool

  1. 裝備 WindCharm
  2. 使用 Tool。
  3. 應觸發同一個 WindDash 流程。
  4. cooldown 應和 Q 鍵共用,不應各算各的。

第三輪:Mobile / Touch

  1. 打開 Device Emulator 或 mobile 測試。
  2. 確認 HUD 上有 DashButton
  3. 點擊 DashButton
  4. 能力應觸發。
  5. cooldown bar 應動畫顯示。

第四輪:Client / Server

  1. Play 中切換到 Server。
  2. 檢查 ServerScriptService.AbilityService 是否存在。
  3. 觀察 Output 中 ability accepted / cooldown rejected 的 server 訊息。
  4. 確認 Humanoid.WalkSpeed 的實際修改來自 server 流程。

這四輪測試能抓到不同類型的錯誤。鍵盤測的是基本流程,Tool 測的是多輸入是否共用 cooldown,mobile 測的是跨裝置,Client / Server 測的是權威邊界。

修正 Prompt

如果手機按鈕不能用,使用:

【Ch16 修正 1|加入 Mobile WindDash】
WindDash 可以使用鍵盤 Q 鍵觸發,但無法在行動裝置使用。

預期結果:
- StarterGui.AdventureHUD 應該包含 DashButton。
- WindDash InputAction 應該有連接到 DashButton 的觸控綁定。
- 按下 DashButton 時,應該和鍵盤 Q 鍵一樣請求同一個 WindDash ability。

請只檢查:
- ReplicatedStorage.Inputs.PlayContext.WindDash
- StarterGui.AdventureHUD
- AbilityHUDController

不要修改 AbilityService 的 server cooldown 邏輯。

如果玩家可以連續加速,使用:

【Ch16 修正 2|套用 WindDash cooldown】
WindDash 目前可以連續觸發,沒有遵守 cooldown。

預期結果:
- WindDash 上一次通過驗證後的 6 秒內,AbilityService 都應該拒絕再次使用。
- cooldown 必須在 server 使用 os.clock() 或同等的 server-side 時間進行檢查。
- HUD 的 cooldown tween 只負責視覺效果,不能作為權威判斷。

請檢查 ServerScriptService.AbilityService。
不要只在 LocalScript 修正這個問題。

如果 Tool 和 Q 鍵 cooldown 不共用,使用:

【Ch16 修正 3|統一能力 cooldown】
WindCharm.Activated 與鍵盤 Q 鍵似乎使用不同的 cooldown。

預期結果:
- Tool.Activated、鍵盤 Q 鍵、gamepad ButtonX 與 DashButton 都應該在 client 呼叫同一個 requestWindDash function。
- 所有請求都應該透過 RequestAbility:FireServer("WindDash") 傳送。
- server 的 AbilityService 應該為每位玩家的 WindDash 維護一份共用 cooldown。

請整合輸入路徑,不要複製 ability 邏輯。

如果 Assistant 把 WalkSpeed 改在 client,使用:

【Ch16 修正 4|將速度權威移到 Server】
WindDash 目前在 LocalScript 中修改 Humanoid.WalkSpeed。

預期結果:
- LocalScript 只能傳送 RequestAbility:FireServer("WindDash") 並更新 HUD。
- ServerScriptService.AbilityService 應該負責驗證及修改 Humanoid.WalkSpeed。

請將實際修改 WalkSpeed 的邏輯移到 server。
client 只保留輸入與視覺回饋。

工程整理

完成本章後,Explorer 應接近這樣:

StarterPack
└── WindCharm

ReplicatedStorage
├── Inputs
│   └── PlayContext
│       └── WindDash
└── Remotes
    ├── RequestGuideHint
    ├── QuestUpdate
    ├── RequestAbility
    └── AbilityUpdate

StarterGui
└── AdventureHUD
    ├── ObjectiveLabel
    ├── HintLabel
    ├── DashButton
    ├── DashCooldownBar
    └── AbilityHUDController

ServerScriptService
├── QuestService
├── EnemyAIService
└── AbilityService

整理時檢查:

  • 能力效果只在 AbilityService
  • AbilityHUDController 不直接改 WalkSpeed
  • WindCharm.Activated 不直接套用能力,只送出同一個 request。
  • RequestAbility 不接受 client 傳入 cooldown、WalkSpeed 或 duration。
  • DashCooldownBar 的 tween 只是視覺。

本章完成後,玩家已經有一個主動能力。接下來第 17 章可以把 Gold 接到商店系統,讓玩家購買或升級這個能力。

本章總結

本章建立了第一個主動能力 WindDash。它表面上只是加速,但背後已經形成一套可延伸的能力架構:

InputAction / Tool / HUD Button
-> RequestAbility
-> AbilityService server validation
-> server-side effect
-> AbilityUpdate HUD feedback

你學到的重點是:

  • 跨裝置輸入要一開始就規劃。
  • Tool 可以代表玩家擁有的能力。
  • client 送出意圖,不決定結果。
  • cooldown 必須在 server。
  • TweenService 適合做視覺,不適合當權威規則。

這套結構會在後面繼續擴充。第 17 章會加入商店與升級,讓玩家用第 14 章任務拿到的 Gold,購買更短 cooldown 或更強的能力效果。


關於 Wolke

嗨!我是 Wolke,曾任 Google Developer Expert(GDE,2019–2023)LINE API Expert

我熱衷於研究 AI Agent、n8n 自動化工作流與全端開發架構,致力於將 AI 技術轉化為真正能落地的生產力工具。

如果你喜歡這篇文章,歡迎透過以下方式與我交流:

📚 技術著作
《實用的 Gemini API 開發點子書》:帶你運用 Gemini App、Google AI Studio、Gemini CLI 與 Antigravity IDE,打造 AI Agent 與實用產品。

📝 技術部落格
歡迎追蹤我的 Medium,我會持續分享 Agentic Automation、架構設計與實際開發的踩坑心得。

🎤 技術講座與合作
我持續受邀至技術社群及研討會,分享 AI Agent、自動化工作流、DevOps 與全端開發實戰。

我曾於 DevOpsDays Taipei 2026 主講「不再只是寫腳本!讓 AI 代理人成為你的 SRE 最佳夥伴」工作坊。

如果你的企業、社群或學校正在尋找相關主題講者,歡迎私訊聯繫,洽談講座與工作坊合作!

🎮 我的 Roblox 遊戲

🎁 免費贈送 OpenAI 或 Claude AI 額度

為了鼓勵大家實際動手打造自己的 Roblox 體驗,我每個月會開放:

  • 10 個名額
  • 每人 50 點 AI 額度
  • 名額送完為止

參加方式:

  1. 訂閱本系列文章。
  2. 分享任一篇系列文章。
  3. 私訊分享截圖及你的 AI 帳號 Email。

確認完成後,我會邀請你加入並設定 50 點額度。名額有限,歡迎把握機會!


上一篇
第 15 章:敵人與簡單 AI
下一篇
第 17 章:商店與升級系統
系列文
Roblox Studio AI 協作開發大全23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言